package com.violet.uaa.server.security.password;

import com.violet.uaa.server.security.factory.UserDetailServiceFactory;
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.Assert;

import javax.annotation.Resource;

/**
 * 类说明: 扩展用户名密码provider
 *
 * @author wqf
 * @date 2024/9/3 9:45
 */
@Setter
@Getter
public class PasswordAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    private UserDetailServiceFactory userDetailsServiceFactory;

    private PasswordEncoder passwordEncoder;

    private volatile String userNotFoundEncodedPassword;

    private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";

    @Resource
    private UserDetailsService userDetailsService;

    /**
     * 允许子类对给定身份验证请求的返回（或缓存）UserDetails执行任何其他检查。
     * 通常，子类至少会将Authentication. getCredentials（）与UserDetails. getPassword（）进行比较。
     * 如果需要自定义逻辑来比较UserDetails和/ 或UsernamePasswordAuthenticationToken的其他属性，
     * 则这些属性也应出现在此方法中。
     */
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {

    }

    /**
     * 允许子类从特定于实现的位置实际检索UserDetails，如果提供的凭据不正确，
     * 可以选择立即抛出AuthenticationException（如果需要以用户身份绑定到资源以获取或生成UserDetails，这尤其有用）。
     * 子类不需要执行任何缓存，因为AbstractUserDetailsAuthenticationProvider默认会缓存UserDetails。
     * UserDetails的缓存确实带来了额外的复杂性，因为这意味着依赖于缓存的后续请求仍需要验证其凭据，即使在此方法中采用基于绑定策略的子类确保了凭据的正确性。
     * 因此，重要的是，子类要么禁用缓存（如果它们想确保此方法是唯一能够对请求进行身份验证的方法，因为不会缓存UserDetails），
     * 要么确保子类实现额外的AuthenticationChecks（UserDetails、UsernamePasswordAuthenticationToken），
     * 以将缓存的UserDetails的凭据与后续的身份验证请求进行比较。
     * 大多数时候，子类不会在此方法中执行凭据检查，而是在附加的AuthenticationChecks（UserDetails、UsernamePasswordAuthenticationToken）中执行，
     * 这样与凭据验证相关的代码就不需要在两个方法之间重复。
     */
    @Override
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        prepareTimingAttackProtection();
        try {
            UserDetails loadedUser = userDetailsServiceFactory.getService(authentication).loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        } catch (UsernameNotFoundException ex) {
            mitigateAgainstTimingAttack(authentication);
            throw ex;
        } catch (InternalAuthenticationServiceException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }

    @Override
    protected void doAfterPropertiesSet() {
        Assert.notNull(this.userDetailsServiceFactory, "A UserDetailsService must be set");
    }

    private void prepareTimingAttackProtection() {
        if (this.userNotFoundEncodedPassword == null) {
            this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
        }
    }

    private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
        if (authentication.getCredentials() != null) {
            String presentedPassword = authentication.getCredentials().toString();
            this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
        }
    }
}
